We want to make a turn-based strategy game, based on a hexagonal grid.

Wait, how do we even lay out a hexagonal grid in kivy?

http://playtechs.blogspot.com/2007/04/hex-grids.html

Also

http://gamedev.stackexchange.com/questions/15881/hexagonal-grid-tiles-tutorials

Ignore device orientation.

Let's lay out a grid. First we need to draw hexagons. Clone gamecamp. change the object names. Can we lay out the basic screen?


In [ ]:
%%file strategygame.kv

#:include debug.kv

<StrategyGame>:
    BoxLayout:
        orientation: 'horizontal'
        DebugLabel:
            text: 'Main map'
            size_hint: .75, 1
        BoxLayout:
            orientation: 'vertical'
            size_hint: .25, 1
            DebugLabel:
                text: 'status'
                size_hint: 1, .66
            DebugLabel:
                text: 'mini-map'
                size_hint: 1, .33

In [ ]:
%%file main.py
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout


class StrategyGame(FloatLayout):
    pass


class StrategyGameApp(App):
    def build(self):
        return StrategyGame()

if __name__ == '__main__':
    StrategyGameApp().run()

In [ ]:


In [ ]:
#!python main.py

Let's make a gridlayout in the game area, and give it a label so we can refer to it from our code. We are going to build a rectangular grid


In [ ]:
%%file strategygame.kv

#:include debug.kv

<StrategyGame>:
    main_map: _main_map
    BoxLayout:
        orientation: 'horizontal'
        GridLayout:
            id: _main_map
            cols: 10
            size_hint: .75, 1
        BoxLayout:
            orientation: 'vertical'
            size_hint: .25, 1
            DebugLabel:
                text: 'status'
                size_hint: 1, .66
            DebugLabel:
                text: 'mini-map'
                size_hint: 1, .33

Now let's hook to that from code. We will do this in __init__ in our main StrategyGame class. Let's make a loop to add buttons to the root widget. We will use properties in the kv file to determine how many rows/cols to add. We will also need to refer to various objects in the widget hierarchy in the code.

You can refer to the top-level object (non-indented) as root in the kv file, but it we want to refer to conveniently elsewhere in the code, we need to use an id.

id: _game

and then from any widget we want to refer to it, we do a

game: _game

We'll use this to access the number of regions to layout in the grid via map_cols and map_rows from the main game widget.

Fill in grids: it fills it in by default, left to right, top to bottom, and give them coordinate references.


In [ ]:
%%file strategygame.kv

#:include debug.kv

<StrategyGame>:
    id: _game
    main_map: _main_map
    map_rows: 10
    map_cols: 10
    BoxLayout:
        orientation: 'horizontal'
        GridLayout:
            id: _main_map
            game: _game
            cols: root.map_cols
            size_hint: .75, 1
        BoxLayout:
            orientation: 'vertical'
            size_hint: .25, 1
            DebugLabel:
                text: 'status'
                size_hint: 1, .66
            DebugLabel:
                text: 'mini-map'
                size_hint: 1, .33

In [ ]:
# %load main.py
from kivy.app import App
from kivy import properties
from kivy.uix import button
from kivy.uix.floatlayout import FloatLayout


class StrategyGame(FloatLayout):
    main_map = properties.ObjectProperty(None)
    map_rows = properties.NumericProperty(0)
    map_cols = properties.NumericProperty(0)

    def __init__(self, **kwargs):
        super(StrategyGame, self).__init__(**kwargs)

        number_of_regions = self.map_rows * self.map_cols
        for region in xrange(0, number_of_regions):
            row = region / self.map_cols
            col = region % self.map_cols
            self.main_map.add_widget(button.Button(text='({}, {})'.format(row, col)))


class StrategyGameApp(App):
    def build(self):
        return StrategyGame()

if __name__ == '__main__':
    StrategyGameApp().run()

We draw alternating rows with the patterns:

/ \
| |
| |

and

\ /
 |
 |

In [ ]:
%%file strategygame.kv

#:include debug.kv

<StrategyGame>:
    id: _game
    main_map: _main_map
    map_rows: 10
    map_cols: 10
    BoxLayout:
        orientation: 'horizontal'
        GridLayout:
            id: _main_map
            game: _game
            cols: root.map_cols
            size_hint: .75, 1
        BoxLayout:
            orientation: 'vertical'
            size_hint: .25, 1
            DebugLabel:
                text: 'status'
                size_hint: 1, .66
            DebugLabel:
                text: 'mini-map'
                size_hint: 1, .33

In [ ]:
%%file main.py
from kivy.app import App
from kivy import properties
from kivy.uix import button
from kivy.uix.floatlayout import FloatLayout


class StrategyGame(FloatLayout):
    main_map = properties.ObjectProperty(None)
    map_rows = properties.NumericProperty(0)
    map_cols = properties.NumericProperty(0)

    def __init__(self, **kwargs):
        super(StrategyGame, self).__init__(**kwargs)

        number_of_regions = self.map_rows * self.map_cols
        for region in xrange(0, number_of_regions):
            row = region / self.map_cols
            col = region % self.map_cols
            self.main_map.add_widget(button.Button(text='({}, {})'.format(row, col)))


class StrategyGameApp(App):
    def build(self):
        return StrategyGame()

if __name__ == '__main__':
    StrategyGameApp().run()

In [ ]:


In [ ]:

Wait, that doesn't draw where we think it should.'


In [ ]:
%%file main.py
import collections

from kivy.app import App
from kivy import properties
from kivy import graphics
from kivy.uix import label
from kivy.uix.floatlayout import FloatLayout


MapCoords = collections.namedtuple('MapCoords', ['row', 'col'])


class StrategyGame(FloatLayout):
    main_map = properties.ObjectProperty(None)
    map_rows = properties.NumericProperty(0)
    map_cols = properties.NumericProperty(0)

    def __init__(self, **kwargs):
        super(StrategyGame, self).__init__(**kwargs)

        number_of_regions = self.map_rows * self.map_cols
        for region in xrange(0, number_of_regions):
            row = region / self.map_cols
            col = region % self.map_cols
            self.main_map.add_widget(HexMapCell(row=row, col=col))


class HexMapCell(label.Label):
    def __init__(self, row=0, col=0, **kwargs):
        self.region_in_map = MapCoords(row, col)
        super(HexMapCell, self).__init__(**kwargs)
        self.draw_hex_edge()

    def draw_hex_edge(self):
        edge = ''
        if self.region_in_map.col % 2 == 0:
            row_mod = self.region_in_map.row % 6
            if row_mod == 0:
                edge = 'BU'
                with self.canvas:
                    graphics.Color(1, 1, 1, 1)
                    graphics.Line(points=[self.x, self.y, self.width + self.x, self.height + self.y])
            elif row_mod in (1, 2):
                edge = 'L '
            elif row_mod == 3:
                edge = 'TD'
            elif row_mod in (4, 5):
                edge = ' R'
        else:
            row_mod = self.region_in_map.row % 6
            if row_mod == 0:
                edge = 'TD'
            elif row_mod in (1, 2):
                edge = ' R'
            elif row_mod == 3:
                edge = 'BU'
            elif row_mod in (4, 5):
                edge = 'L '

        self.text = edge

class StrategyGameApp(App):
    def build(self):
        return StrategyGame()

if __name__ == '__main__':
    StrategyGameApp().run()

In [ ]:
%%file main.py
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.lang import Builder
from kivy.properties import NumericProperty

class HelloWorld(GridLayout):
    cols = NumericProperty(4)
    def __init__(self, **kw):
        super(HelloWorld, self).__init__(**kw)
        self.add_widget(self.BU())
        self.add_widget(self.TD())
        self.add_widget(self.BU())
        self.add_widget(self.TD())

    def BU(self):
        return Builder.load_string('''
Label:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.x, self.y, self.right, self.top)
            width: 2''')

    def TD(self):
        return Builder.load_string('''
Label:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.x, self.top, self.right, self.y)
            width: 2''')

    def L(self):
        return Builder.load_string('''
Label:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.x, self.y, self.x, self.top)
            width: 2''')

    def R(self):
        return Builder.load_string('''
Label:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.right, self.y, self.right, self.top)
            width: 2''')


class HelloWorldApp(App):
    def build(self):
        return HelloWorld()

if __name__ == '__main__':
    HelloWorldApp().run()

In [ ]:
# %load strategygame.kv

#:include debug.kv

<StrategyGame>:
    id: _game
    main_map: _main_map
    map_rows: 10
    map_cols: 10
    BoxLayout:
        orientation: 'horizontal'
        GridLayout:
            id: _main_map
            game: _game
            cols: root.map_cols
            size_hint: .75, 1
        BoxLayout:
            orientation: 'vertical'
            size_hint: .25, 1
            DebugLabel:
                text: 'status'
                size_hint: 1, .66
            DebugLabel:
                text: 'mini-map'
                size_hint: 1, .33

Trying to draw the hexmap

We tried adding the ability to style our widgets for to draw the Hexmap using lines like this

BU: /
TD: \
L: |
R:   |

We get hexs now, but we need to figure out how to do the aspect ratio properly now...for a spiky top, we need each grid widget to have aspec ratio $$\sqrt{3}\cdot height = width$$


In [ ]:
%%file strategygame.kv

#:include debug.kv

<StrategyGame>:
    id: _game
    main_map: _main_map
    map_rows: 10
    map_cols: 10
    BoxLayout:
        orientation: 'horizontal'
        GridLayout:
            id: _main_map
            game: _game
            cols: root.map_cols
            size_hint: .75, 1
        BoxLayout:
            orientation: 'vertical'
            size_hint: .25, 1
            DebugLabel:
                text: 'status'
                size_hint: 1, .66
            DebugLabel:
                text: 'mini-map'
                size_hint: 1, .33

<BU>:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.x, self.y, self.right, self.top)
            width: 2
<TD>:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.x, self.top, self.right, self.y)
            width: 2
<L>:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.x, self.y, self.x, self.top)
            width: 2
<R>:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.right, self.y, self.right, self.top)
            width: 2

In [ ]:
%%file main.py
import collections

from kivy.app import App
from kivy import properties
from kivy import graphics
from kivy.uix import label
from kivy.uix.floatlayout import FloatLayout
import math

MapCoords = collections.namedtuple('MapCoords', ['row', 'col'])


class StrategyGame(FloatLayout):
    main_map = properties.ObjectProperty(None)
    map_rows = properties.NumericProperty(0)
    map_cols = properties.NumericProperty(0)

    def __init__(self, **kwargs):
        super(StrategyGame, self).__init__(**kwargs)

        number_of_regions = self.map_rows * self.map_cols
        for region in xrange(0, number_of_regions):
            row = region / self.map_cols
            col = region % self.map_cols
            self.main_map.add_widget(self.pick_hex_cell(row=row, col=col))


    def pick_hex_cell(self, row, col):
        row_mod = row % 6
        if col % 2 == 0:
            if row_mod == 0:
                return BU()
            elif row_mod in (1, 2):
                return L()
            elif row_mod == 3:
                return TD()
            elif row_mod in (4, 5):
                return R()
        else:
            if row_mod == 0:
                return TD()
            elif row_mod in (1, 2):
                return R()
            elif row_mod == 3:
                return BU()
            elif row_mod in (4, 5):
                return L()


class HexMapCell(label.Label):
    def __init__(self, row=0, col=0, **kwargs):
        self.region_in_map = MapCoords(row, col)
        super(HexMapCell, self).__init__(**kwargs)

class BU(HexMapCell):
    pass
class TD(HexMapCell):
    pass
class L(HexMapCell):
    pass
class R(HexMapCell):
    pass


class StrategyGameApp(App):
    def build(self):
        return StrategyGame()

if __name__ == '__main__':
    StrategyGameApp().run()

Replaced the debug labels and coloured the status and minimap two different ways.


In [ ]:
%%file strategygame.kv
#:import math math

<StrategyGame>:
    id: _game
    main_map: _main_map
    map_rows: 30
    map_cols: 10
    BoxLayout:
        orientation: 'horizontal'
        GridLayout:
            id: _main_map
            game: _game
            cols: root.map_cols
            size_hint: .75, 1
        BoxLayout:
            orientation: 'vertical'
            size_hint: .25, 1
            Label:
                id: _stats
                text: 'status'
                size_hint: 1, .66
                canvas.before:
                    Color:
                        rgba: .49, .49, .81, 1
                    Rectangle:
                        pos: _stats.pos
                        size: _stats.size
            Button:
                text: 'mini-map'
                size_hint: 1, .33
                background_color: .75, .71, .99, 1


<BU>:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.x, self.y, self.right, self.top)
            width: 2
<TD>:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.x, self.top, self.right, self.y)
            width: 2
<L>:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.x, self.y, self.x, self.top)
            width: 2
<R>:
    canvas:
        Color:
            rgba: (1,1,1,1)
        Line:
            points: (self.right, self.y, self.right, self.top)
            width: 2

<HexMapCell>:
    size_hint: 1, None
    height: self.width / math.sqrt(3)

In [ ]:
%%file main.py
import collections
import random

from kivy import app, properties
from kivy.uix import button, label
from kivy.uix.floatlayout import FloatLayout

MapCoords = collections.namedtuple('MapCoords', ['row', 'col'])


class StrategyGame(FloatLayout):
    main_map = properties.ObjectProperty(None)
    map_rows = properties.NumericProperty(0)
    map_cols = properties.NumericProperty(0)

    def __init__(self, **kwargs):
        super(StrategyGame, self).__init__(**kwargs)

        number_of_regions = self.map_rows * self.map_cols
        for region in xrange(0, number_of_regions):
            row = region / self.map_cols
            col = region % self.map_cols

            # Add hex cells to make up the map.
            hex_cell = self.pick_hex_cell(row=row, col=col)
            self.main_map.add_widget(hex_cell)

            # Add overlay conditionally.
            if (row % 6 == 2 and col % 2 == 0) or (row % 6 == 5 and col % 2 == 1):
                print('({}, {})'.format(row, col))
                self.add_widget(HexMapControlCell(hex_bind=hex_cell))

    @staticmethod
    def pick_hex_cell(row, col):
        row_mod = row % 6
        if col % 2 == 0:
            if row_mod == 0:
                return BU()
            elif row_mod in (1, 2):
                return L()
            elif row_mod == 3:
                return TD()
            elif row_mod in (4, 5):
                return R()
        else:
            if row_mod == 0:
                return TD()
            elif row_mod in (1, 2):
                return R()
            elif row_mod == 3:
                return BU()
            elif row_mod in (4, 5):
                return L()


class HexMapCell(label.Label):
    def __init__(self, row=0, col=0, **kwargs):
        super(HexMapCell, self).__init__(**kwargs)
        self.coords = MapCoords(row, col)


class BU(HexMapCell):
    pass


class TD(HexMapCell):
    pass


class L(HexMapCell):
    pass


class R(HexMapCell):
    pass


class HexMapControlCell(button.Button):
    def __init__(self, hex_bind=None, **kwargs):
        super(HexMapControlCell, self).__init__(**kwargs)
        self.hex_bind = hex_bind
        self.background_color = random.random(), random.random(), random.random(), 1
        self.bind(pos=self.reposition_control_cell, size=self.resize_control_cell)
        self.text = '({}, {})'.format(self.hex_bind.coords.row, self.hex_bind.coords.col)

    def reposition_control_cell(self, obj, value):
        self.pos = self.hex_bind.pos

    def resize_control_cell(self, obj, value):
        self.height = self.hex_bind.height * 2
        self.width = self.hex_bind.width * 2


class StrategyGameApp(app.App):
    def build(self):
        return StrategyGame()

if __name__ == '__main__':
    StrategyGameApp().run()

Warning if you run this your will get an error!!

We should probably have a viewer and a hexagon map underneath!!

But not for now...let's make an overlay of boxes...and label them with an "offset-r)

Hmmmm....overlay...is...having...issues.


In [ ]:
%%file strategygame.kv
#:import math math
#:include debug.kv

<StrategyGame>:
    id: _game
    main_map: _main_map
    map_rows: 30
    map_cols: 10
    BoxLayout:
        orientation: 'horizontal'
        GridLayout:
            id: _main_map
            game: _game
            cols: root.map_cols
            size_hint: .75, 1
        BoxLayout:
            orientation: 'vertical'
            size_hint: .25, 1
            Label:
                id: _stats
                text: 'status'
                size_hint: 1, .66
                canvas.before:
                    Color:
                        rgba: .49, .49, .81, 1
                    Rectangle:
                        pos: _stats.pos
                        size: _stats.size
            Button:
                text: 'mini-map'
                size_hint: 1, .33
                background_color: .75, .71, .99, 1

<Hex@Label>:
    pos_hint: {'center_x':.5, 'center_y':.5}
    canvas.after:
        Color:
            rgba: 1,1,1,1
        Ellipse:
            segments: 6
            pos: self.pos
            size: min(self.width, self.height), min(self.width, self.height)

<HexMapCell>:
    size_hint: 1, None
    height: self.width / math.sqrt(3)

In [ ]:
%%file main.py
import collections
import random
import math
from kivy import app, properties
from kivy.uix import button, label
from kivy.uix.floatlayout import FloatLayout
from kivy.graphics import Color, Ellipse, Line
from kivy.logger import Logger

MapCoords = collections.namedtuple('MapCoords', ['row', 'col'])


class StrategyGame(FloatLayout):
    main_map = properties.ObjectProperty(None)
    map_rows = properties.NumericProperty(0)
    map_cols = properties.NumericProperty(0)

    def __init__(self, **kwargs):
        super(StrategyGame, self).__init__(**kwargs)

        number_of_regions = self.map_rows * self.map_cols
        for region in xrange(0, number_of_regions):
            row = region / self.map_cols
            col = region % self.map_cols

            # Add hex cells to make up the map.
            hex_cell = self.pick_hex_cell(row=row, col=col)
            self.main_map.add_widget(hex_cell)

            # Add overlay conditionally.
            if (row % 6 == 1 and col % 2 == 1) or (row % 6 == 4 and col % 2 == 0):
                print('({}, {})'.format(row, col))
                #radius = math.sqrt(hex_cell.width**2 + hex_cell.height**2)
                radius = 2*hex_cell.height
                with hex_cell.canvas.after:
                    Color(1,0,1,1)
                    hex_cell.ell = Line(circle=(hex_cell.x, hex_cell.y,radius, 0, 360, 6), width=2)
                hex_cell.bind(pos=hex_cell.update_pos, size=hex_cell.update_pos)


    @staticmethod
    def pick_hex_cell(row, col):
        row_mod = row % 6
        if col % 2 == 0:
            if row_mod == 0:
                return BU()
            elif row_mod in (1, 2):
                return L()
            elif row_mod == 3:
                return TD()
            elif row_mod in (4, 5):
                return R()
        else:
            if row_mod == 0:
                return TD()
            elif row_mod in (1, 2):
                return R()
            elif row_mod == 3:
                return BU()
            elif row_mod in (4, 5):
                return L()



class HexMapCell(label.Label):
    def __init__(self, row=0, col=0, **kwargs):
        super(HexMapCell, self).__init__(**kwargs)
        self.coords = MapCoords(row, col)

    def update_pos(self, instance, value):
        Logger.info("StratGame: {}".format(instance))
        #radius = math.sqrt(self.width**2 + self.height**2)
        radius = 2*self.height
        self.ell.circle = (self.x, self.y, radius, 0, 360, 6)



class BU(HexMapCell):
    pass


class TD(HexMapCell):
    pass


class L(HexMapCell):
    pass


class R(HexMapCell):
    pass



class HexMapControlCell(button.Button):
    def __init__(self, hex_bind=None, **kwargs):
        super(HexMapControlCell, self).__init__(**kwargs)
        self.hex_bind = hex_bind
        self.background_color = random.random(), random.random(), random.random(), 1
        self.bind(pos=self.reposition_control_cell, size=self.resize_control_cell)
        self.text = '({}, {})'.format(self.hex_bind.coords.row, self.hex_bind.coords.col)

    def reposition_control_cell(self, obj, value):
        self.pos = self.hex_bind.pos

    def resize_control_cell(self, obj, value):
        self.height = self.hex_bind.height * 2
        self.width = self.hex_bind.width * 2


class StrategyGameApp(app.App):
    def build(self):
        return StrategyGame()

if __name__ == '__main__':
    StrategyGameApp().run()

What did we learn?

  • angle 0 appears to be straight up for a circle or ellipse
  • The radius of the hexagon is $2*height$ for a pointed top hexagon, and for flat top, the radius of the hexagon is $2*width$
  • Make sure you fix the aspect ratio of the rectangles you're basing things now
  • segments are inscribed inside the circle
  • tiling problems are hard!!! get a mathematician.
  • there is a utility for hex codes for colours
  • draw the hexagon using a Line with a circle to make an outline instead of an Ellipse

Cleaned some stuff up and added colours to the hexagons.


In [ ]: